﻿/* 
   Copyright 2011 LUCA Studios LLC & John Evans

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

namespace LUCAStudios.Silverlight.Controls.DragDrop
{
    using System;
    using System.Linq;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Media;
    using System.Collections.Generic;
    using System.Windows.Input;
    using Draggable;

    /// <summary>
    /// Provides a control flow for dragging/dropping operations in the LUCA UI Framework
    /// </summary>
    public class DragDropManager : DraggableBase
    {
        private readonly HashSet<IDragDropSource> _sources = new HashSet<IDragDropSource>();
        private readonly HashSet<IDragDropTarget> _targets = new HashSet<IDragDropTarget>();

        private readonly Canvas _dropTargetLayout = new Canvas
        {
            Background = new SolidColorBrush(Colors.Transparent)
        };

        private object _currentDragDropData;

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="rootPanel">Passing the root panel which will be used to track mouse move events, etc</param>
        public DragDropManager(Panel rootPanel)
            : base(rootPanel) { }

        /// <summary>
        /// Generally only used for debugging, will visually mark drop zones during a drag/drop operation
        /// </summary>
        public bool ShowDropTargets { get; set; }

        /// <summary>
        /// Fires on left mouse down
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected override void DragMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            var source = sender as FrameworkElement;
            if (source == default(FrameworkElement)) return;

            var dds = ClickTargetLookup[source] as IDragDropSource;
            if (dds == null) return;

            GenerateDropTargets(dds);

            base.DragMouseLeftButtonDown(sender, e);
        }

        /// <summary>
        /// Fires on left button up
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected override void RootPanelMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            RemoveDropTargets();
            _currentDragDropData = null;
            base.RootPanelMouseLeftButtonUp(sender, e);
        }

        /// <summary>
        /// Firest on mouse leave
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected override void RootPanelMouseLeave(object sender, MouseEventArgs e)
        {
            RemoveDropTargets();
            _currentDragDropData = null;
            base.RootPanelMouseLeave(sender, e);
        }

        /// <summary>
        /// fires on left button up
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void DropZoneMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            e.Handled = true;
            RemoveDropTargets();
            var dz = sender as DropZoneBase;
            OnDropped(dz);
            base.RootPanelMouseLeftButtonUp(sender, e);
        }

        /// <summary>
        /// Called when IDragDropSource is dropped on a drop target
        /// </summary>
        /// <param name="whichZone"></param>
        protected virtual void OnDropped(DropZoneBase whichZone)
        {
            if (whichZone == null) throw new ArgumentNullException("whichZone");

            if (_currentDragDropData is IDragDropSource)
            {
                ((IDragDropSource)_currentDragDropData).OnDropped(whichZone.Source);
            }
            whichZone.Source.OnDrop(whichZone, _currentDragDropData);
            _currentDragDropData = null;
        }

        /// <summary>
        /// Generates drop targets when a drag operation is about to begin
        /// </summary>
        /// <param name="source"></param>
        protected void GenerateDropTargets(IDragDropSource source)
        {
            if (RootPanel.Children.Contains(_dropTargetLayout))
                RemoveDropTargets();

            _currentDragDropData = source.DragDropData;

            //source cannot be its own target during drag, so filter it out of the iteration, if exists
            //var dzs = _targets
            //    .Where(o => !(source.Equals(o)));
            //.OrderBy(o => o.ZOrder)
            //.SelectMany(o => o.GetDropZones(source.DragDropData));

            //NOTE: Using nested foreach instead of .SelectMany (commented out above)
            //NOTE: because .SelectMany seems to be making duplicate calls to o.GetDropZones(..)

            _targets
                .Where(o => !(source.Equals(o)))
                .ToObservable()
                .Subscribe(ddTarget =>
                        {
// ReSharper disable ConvertToLambdaExpression
                            ddTarget.GetDropZones(source.DragDropData)
                                .ToObservable()
                                .Subscribe(dropZone =>
                                               {
#if DEBUG
                                                   //color up the hotspots during debug for visual reference
                                                   if (ShowDropTargets)
                                                   {
                                                       var c = (byte)(200);
                                                       ((Grid)dropZone.HotSpot).Background = new SolidColorBrush(Color.FromArgb(150, c, c, c));
                                                   }
#endif

                                                   //Canvas.SetZIndex(dz.DropZoneUI, dz.Source.ZOrder);
                                                   dropZone.MouseLeftButtonUp += DropZoneMouseLeftButtonUp;
                                                   _dropTargetLayout.Children.Add(dropZone.DropZoneUI);
                                               });
// ReSharper restore ConvertToLambdaExpression
                        });

            RootPanel.Children.Add(_dropTargetLayout);
        }

        /// <summary>
        /// removes drop targets
        /// </summary>
        protected void RemoveDropTargets()
        {
            if (RootPanel.Children.Contains(_dropTargetLayout))
                RootPanel.Children.Remove(_dropTargetLayout);

            if (!_dropTargetLayout.Children.Any()) return;

            _dropTargetLayout
                .Children
                .OfType<DropZoneBase>()
                .ToObservable()
                .Subscribe(dpi =>
                               {
                                   dpi.MouseLeftButtonUp -= DropZoneMouseLeftButtonUp;
                                   dpi.Dispose();
                               });

            _dropTargetLayout.Children.Clear();
        }

        /// <summary>
        /// Crawls the VisualTree of the given UIElement and registers all 
        /// elements participating in the DragDrop framework.
        /// </summary>
        /// <param name="startingWith"></param>
        public void RegisterAllDragDropParticipants(UIElement startingWith)
        {
            startingWith
                .FindAllElementsByType<IDragDropSource>()
                .Where(o => !_sources.Contains(o))
                .Subscribe(RegisterDragDropSource);

            startingWith
                .FindAllElementsByType<IDragDropTarget>()
                .Where(o => !_targets.Contains(o))
                .Subscribe(RegisterDragDropTarget);
        }

        /// <summary>
        /// Registers a IDragDropTarget to the manager
        /// </summary>
        /// <param name="dragDropTarget"></param>
        public void RegisterDragDropTarget(IDragDropTarget dragDropTarget)
        {
            if (dragDropTarget == null) throw new ArgumentNullException("dragDropTarget");

            _targets.Add(dragDropTarget);
        }

        /// <summary>
        /// Unregisters a IDragDropTarget from the manager
        /// </summary>
        /// <param name="dragDropTarget"></param>
        public void UnRegisterDragDropTarget(IDragDropTarget dragDropTarget)
        {
            if (dragDropTarget == null) throw new ArgumentNullException("dragDropTarget");

            if (_targets.Contains(dragDropTarget))
                _targets.Remove(dragDropTarget);
        }

        /// <summary>
        /// Registers a IDragDropSource to the manager
        /// </summary>
        /// <param name="dragDropSource"></param>
        public void RegisterDragDropSource(IDragDropSource dragDropSource)
        {
            if (dragDropSource == null) throw new ArgumentNullException("dragDropSource");

            _sources.Add(dragDropSource);
            RegisterDraggable(dragDropSource);
        }

        /// <summary>
        /// Unregisters a IDragDropSource from the manager
        /// </summary>
        /// <param name="dragDropSource"></param>
        public void UnRegisterDragDropSource(IDragDropSource dragDropSource)
        {
            if (dragDropSource == null) throw new ArgumentNullException("dragDropSource");

            if (_sources.Contains(dragDropSource))
            {
                _sources.Remove(dragDropSource);
                UnRegisterDraggable(dragDropSource);
            }
        }
    }
}
